Skip to content

fix(formal): make the local Coq assumptions gate capable of failing - #202

Merged
hyperpolymath merged 2 commits into
mainfrom
fix/coq-gate-honest
Jul 28, 2026
Merged

fix(formal): make the local Coq assumptions gate capable of failing#202
hyperpolymath merged 2 commits into
mainfrom
fix/coq-gate-honest

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

fix(formal): make the local Coq assumptions gate capable of failing

just -f formal/Justfile check-assumptions returned 0 whether or not
anything compiled, so a green local proof gate was not evidence of a
proof. Demonstrated by putting a stub coqc that exits 127 on PATH:

$ PATH=/tmp/fakebin:$PATH just -f formal/Justfile check-planner
coqc: not found
OK(Planner): Q1-lite 3 theorems x 4 axioms whitelisted
exit=0

Cause: every compile recipe was coqc -q X.v | tee X.out. A pipeline's
exit status is its last command's, so just saw tee's 0 and moved
on; coqc's diagnostics go to stderr, which tee never captures. The
guard then ran awk over an empty X.out, found no axioms, and reported OK.

Fixed by redirecting instead of piping: coqc -q X.v > X.out. The exit
status is now coqc's and just aborts the recipe.

Note this is deliberately NOT fixed with set shell := [... "pipefail" ...].
The guards are shaped awk | sort -u | grep -vE '<whitelist>' | { ... },
and grep -v exits 1 precisely when it filters everything out — i.e. on
the success path, where every axiom is whitelisted. Enabling pipefail
would turn every passing check into a failure. .github/workflows/coq-build.yml
already handles this correctly (set -euo pipefail and || true on each
grep), so CI was never affected; this was a local-only divergence.

Also adds Set Printing Width 400. to all nine modules. Both the CI and
Justfile guards match axiom names with awk '/^[A-Za-z_][A-Za-z0-9_]* :/',
which only fires when the name and its colon share a line. Coq wraps at
~78 columns and Planner.out's longest line was 77 — one character
from silently escaping the check. With the directive, statements print in
full (Normalizer 75 -> 157, WAL 62 -> 127, Octad 62 -> 127, PlannerSemantic
74 -> 113, Drift 72 -> 95).

To be precise about what this did and did not find: comparing the axiom
names visible to the guard with and without the directive shows no
difference today
. Nothing was being hidden; this closes a latent escape
before it opens, it does not fix an active one.

Verified: with a broken coqc the gate now exits 127 ("recipe planner
failed on line 29"); with the real coqc 8.20.1 all nine modules compile and
check-assumptions exits 0 with 9/9 OK lines. reuse lint still compliant.

`just -f formal/Justfile check-assumptions` returned 0 whether or not
anything compiled, so a green local proof gate was not evidence of a
proof. Demonstrated by putting a stub `coqc` that exits 127 on PATH:

    $ PATH=/tmp/fakebin:$PATH just -f formal/Justfile check-planner
    coqc: not found
    OK(Planner): Q1-lite 3 theorems x 4 axioms whitelisted
    exit=0

Cause: every compile recipe was `coqc -q X.v | tee X.out`. A pipeline's
exit status is its *last* command's, so `just` saw `tee`'s 0 and moved
on; `coqc`'s diagnostics go to stderr, which `tee` never captures. The
guard then ran awk over an empty `X.out`, found no axioms, and reported OK.

Fixed by redirecting instead of piping: `coqc -q X.v > X.out`. The exit
status is now `coqc`'s and `just` aborts the recipe.

Note this is deliberately NOT fixed with `set shell := [... "pipefail" ...]`.
The guards are shaped `awk | sort -u | grep -vE '<whitelist>' | { ... }`,
and `grep -v` exits 1 precisely when it filters everything out — i.e. on
the success path, where every axiom is whitelisted. Enabling pipefail
would turn every passing check into a failure. `.github/workflows/coq-build.yml`
already handles this correctly (`set -euo pipefail` *and* `|| true` on each
grep), so CI was never affected; this was a local-only divergence.

Also adds `Set Printing Width 400.` to all nine modules. Both the CI and
Justfile guards match axiom names with `awk '/^[A-Za-z_][A-Za-z0-9_]* :/'`,
which only fires when the name and its colon share a line. Coq wraps at
~78 columns and `Planner.out`'s longest line was **77** — one character
from silently escaping the check. With the directive, statements print in
full (Normalizer 75 -> 157, WAL 62 -> 127, Octad 62 -> 127, PlannerSemantic
74 -> 113, Drift 72 -> 95).

To be precise about what this did and did not find: comparing the axiom
names visible to the guard with and without the directive shows **no
difference today**. Nothing was being hidden; this closes a latent escape
before it opens, it does not fix an active one.

Verified: with a broken `coqc` the gate now exits 127 ("recipe `planner`
failed on line 29"); with the real coqc 8.20.1 all nine modules compile and
`check-assumptions` exits 0 with 9/9 OK lines. `reuse lint` still compliant.
Comment thread formal/Drift.v
@gitar-bot

gitar-bot Bot commented Jul 28, 2026

Copy link
Copy Markdown

Note

Automatic reviews are paused because your trial's included automatic processing has been used for this period. Upgrade now, or comment "Gitar review" to run a review anytime.
Learn more

Code Review ✅ Approved 1 resolved / 1 findings

Fixes the local Coq assumptions gate by replacing stdout piping with redirection to correctly capture and propagate coqc exit statuses, while adding Printing Width directives. Consider cleaning up the duplicated comment block across all nine modules.

Auto-approved and auto-merge armed: No blocking issues found.
Please see Auto-approve Docs for details on setting custom approval criteria. — merges when pipeline and required approvals pass.

✅ 1 resolved
Quality: Duplicated Set Printing Width comment block in all 9 modules

📄 formal/Drift.v:30-37 📄 formal/Octad.v:93-100 📄 formal/Normalizer.v 📄 formal/Transaction.v 📄 formal/VCL.v 📄 formal/Planner.v 📄 formal/PlannerSemantic.v 📄 formal/Provenance.v 📄 formal/WAL.v
Each of the nine .v files adds the Set Printing Width 400. explanatory comment twice: once immediately above the directive (correct) and again as a stray copy with no directive following it (e.g. Drift.v lines 35-37, Octad.v lines 98-100). The second block is a dangling duplicate that adds noise and could mislead a reader into thinking a second directive is missing. Remove the redundant comment block from each module, leaving only the one that precedes Set Printing Width 400..

Options

Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

@gitar-bot

gitar-bot Bot commented Jul 28, 2026

Copy link
Copy Markdown

⚠️ Gitar auto-approved this PR but could not enable auto-merge: auto-merge is disabled for this repository — enable "Allow auto-merge" in the repository settings.

@gitar-bot gitar-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gitar has auto-approved this PR and enabled auto-merge (configure)

@gitar-bot gitar-bot Bot added the gitar-approved Added by Gitar label Jul 28, 2026
@hyperpolymath
hyperpolymath marked this pull request as ready for review July 28, 2026 16:08
@hyperpolymath
hyperpolymath merged commit c572d48 into main Jul 28, 2026
39 checks passed
@hyperpolymath
hyperpolymath deleted the fix/coq-gate-honest branch July 28, 2026 16:09
hyperpolymath added a commit that referenced this pull request Jul 28, 2026
…ules) (#205)

docs(formal): remove the duplicated Set Printing Width comment (9
modules)

#202 landed the same three-line explanatory comment twice in every
module —
once above `Set Printing Width 400.` where it belongs, and once orphaned
immediately below it. My fault: I ran the insertion, then deleted only
the
`Set Printing Width 400.` line to A/B-test whether the ~78-column wrap
was
actually hiding any axiom from the guard, then re-ran the insertion. The
sed
that undid the test matched only the directive, not its comment, so the
second
pass stacked a fresh copy on top of the leftover.

Comment-only; no directive, no proof and no guard changes.
`formal/Justfile`
is untouched.

To be accurate about scope, since the obvious worry is that a text edit
near
the imports clipped something: it did not. `Import ListNotations.` and
every
`Require Import` are intact in all nine modules — verified before and
after.
(A *different* cleanup attempt of mine did briefly eat that import by
skipping
a fixed three lines past the marker, which in `Planner.v` overran the
comment.
That version was reset and never committed; it is mentioned only so the
next
person knows the offset-based approach is the wrong tool here. This
commit
removes the second matched block specifically, keyed on occurrence
count.)

Verified: real coqc 8.20.1 -> `check-assumptions` exits 0, 9/9 OK; stub
coqc
-> exit 127, so the gate from #202 still fails when it should;
`Normalizer.out`
still prints at 157 columns, so the wrap fix is unaffected; `reuse lint`
compliant.
hyperpolymath added a commit that referenced this pull request Jul 28, 2026
…n both modules (#206)

feat(formal): discharge optimize_is_permutation — axiom to theorem, in
both modules

`optimize_is_permutation` was an `Axiom` in `formal/Planner.v` and,
separately,
a second identical `Axiom` in `formal/PlannerSemantic.v`. Both are now
gone.

**It was not merely unproven — it was unprovable.** `optimize` was a
`Parameter`, so nothing constrained it: `fun _ => []` inhabits the
parameter and
refutes `forall lp, Permutation lp (optimize lp)`. Assuming the
statement was
assuming the conclusion, and no amount of proof effort against that
signature
would have worked. Discharging it *requires* replacing the parameter
with a
definition, which is what this does:

* `modality` becomes an `Inductive` with the six constructors of
`crate::Modality`, and `execution_priority` transcribes the Rust match
arms
(Temporal 10, Vector 20, Document 30, Graph 40, Tensor 50, Semantic 90).
* `optimize` becomes `Coq.Sorting.Mergesort`'s `sort` under `node_leb` —
  execution priority first, cost as tie-breaker, i.e. the key
  `Optimizer::optimize`'s `sort_by` comparator actually uses.
* The permutation property falls out of the standard library's
`Permuted_sort`,
  which is itself closed under the global context.

All five call sites were left untouched and still compile: the theorem
statement is character-for-character what the axiom asserted.

`PlannerSemantic.v` claimed in a comment to be "reusing Planner.v
abstractions"
while in fact re-declaring its own `modality`, `condition`, `node`,
`optimize`
and a duplicate axiom. The two files agreed only by coincidence, and the
duplicate meant discharging Planner's axiom would have left an identical
assumption standing next door. It now genuinely `Require Import
Planner`, and
`formal/Justfile` gains the `planner-semantic: planner` dependency edge
that
this makes necessary.

**Whitelists tightened — this is the acceptance test, not bookkeeping.**
The
guards enforce "exactly these assumptions", never "no assumptions", so
leaving
`optimize_is_permutation` listed would have let it return silently:

* Planner: `modality|condition|optimize|optimize_is_permutation` ->
`condition|node_cost`
* PlannerSemantic: drops `modality`, `optimize`,
`optimize_is_permutation`

Both `formal/Justfile` and `.github/workflows/coq-build.yml` updated
together.

Two `Parameter`s remain **by design**, and being parametric in them
makes the
result stronger, not weaker — it holds for *any* condition
representation and
*any* cost function: `condition` (the Rust `ConditionKind`'s nine
constructors
are irrelevant to whether nodes are reordered or lost) and `node_cost`
(abstracts `CostModel::estimate`'s `time_ms`).

Two honest model-vs-code gaps, both recorded in the module header rather
than
elided, neither affecting the permutation property:

* `node_cost` returns `nat`; the Rust tie-breaker is `f64` compared with
  `partial_cmp(...).unwrap_or(Equal)`. A NaN would make that comparator
non-transitive and violate `sort_by`'s contract. NaN is unreachable
today —
the only division in `CostModel::estimate` is guarded and feeds
`selectivity`,
  not `time_ms` — but nothing in the Rust types enforces that.
* Rust `optimize` is partial (`PlannerError::EmptyPlan`); `sort []` is
`[]`.

`exec_node_comm` deliberately stays. It is a genuine spec axiom over an
uninterpreted `exec_node` and needs per-operator semantics to discharge
— not
something this change touches, and it should not look like collateral.

Verified:
* Clean build: `just -f formal/Justfile all` and `check-assumptions`
both exit 0,
  9/9 OK, with the tightened whitelists.
* `optimize_is_permutation` appears zero times in either module's
`Print Assumptions` output, and no `Axiom optimize_is_permutation`
remains
  anywhere (was 2).
* **Negative control** — reintroducing an axiom a theorem depends on is
caught:
`ERROR(Planner): unexpected axiom: sneaky_regression`, gate exit 1. The
  tightened whitelist genuinely fails rather than passing vacuously.
* Stub `coqc` still drives the gate to exit 127, so #202's fix is
intact.
* `reuse lint` compliant.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gitar-approved Added by Gitar

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant